Анализ поведения пользователей мобильного приложения¶
Содержание
- 1 Загрузка данных и подготовка к анализу
- 2 Предобработка данных
- 3 Исследование и проверка данных
- 4 Изучение воронки событий
- 5 Изучение результатов A/A/B-теста
- 6 Выводы
Краткое описание проекта: стартап продает продукты питания через свое мобильное приложение. Необходимо разобраться в поведении пользователей, изучить воронку продаж. Также необходимо исследовать результат эксперимента по внедрению новых шрифтов в приложение, в рамках которого все пользователи были поделены на 2 контрольную и 1 экспериментальную группы.
Описание данных:
EventName— название события;DeviceIDHash— уникальный идентификатор пользователя;EventTimestamp— время события;ExpId— номер эксперимента: 246 и 247 — контрольные группы, а 248 — экспериментальная.
План исследования:
- Выгрузка данных и первичное знакомство с данными.
- Изменение типов данных, обработка дубликатов и пропусков, добавление новых столбцов.
- Проверка корректности подготовки к проведению A/A/B-теста.
- Исследование и проверка данных.
- Изучение воронки событий.
- Сравнение результатов A/A теста для контрольных групп.
- Сравнение результатов A/A/B теста для экспериментальной и контрольных групп.
- Принятие решения по результатам A/A/B-теста.
- Выводы.
Цель проекта: рассмотрение целесообразности изменения шрифтов в мобильном приложении посредством изучения потребительского поведения.
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
import datetime as dt
import plotly.express as px
from scipy import stats as st
from statsmodels.stats import proportion as pr
from plotly import graph_objects as go
from scipy.stats import binom, norm
from math import sqrt
from IPython.display import display
from IPython.display import Image
Загрузка данных и подготовка к анализу¶
logs = pd.read_csv('logs_exp.csv', sep = '\t')
logs.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 244126 entries, 0 to 244125 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 EventName 244126 non-null object 1 DeviceIDHash 244126 non-null int64 2 EventTimestamp 244126 non-null int64 3 ExpId 244126 non-null int64 dtypes: int64(3), object(1) memory usage: 7.5+ MB
Предобработка данных¶
Замена названий и добавление новых столбцов¶
logs.columns = ['event', 'user_id', 'timestamp', 'group']
logs['datetime'] = logs['timestamp'].map(lambda x: dt.datetime.fromtimestamp(x))
logs['dt'] = logs['datetime'].dt.date
logs['dt'] = pd.to_datetime(logs['dt'])
Обработка пропусков¶
Из первичного знакомства с данными видно, что пропуски отсутствуют.
Обработка дубликатов¶
print('Количество дубликатов:', logs.duplicated().sum())
print('Доля дубликатов:', round(logs.duplicated().sum()/len(logs)*100,2),'%')
Количество дубликатов: 413 Доля дубликатов: 0.17 %
В данных обнаружено 413 явных дубликатов, что составляет примерно 0,17% от общего количества записей. Поэтому дубликаты лучше удалить.
logs = logs.drop_duplicates()
Проверка правильности подготовки к A/A/B-тесту¶
Проверим корректность деления трафика, в идеале пользователей должно быть равное количество в каждой из 3 групп:
logs.groupby(by='group').agg({'user_id': 'nunique'})
| user_id | |
|---|---|
| group | |
| 246 | 2489 |
| 247 | 2520 |
| 248 | 2542 |
print('Относительная разница между группой 248 и 246:',
round(logs.query('group==248')['user_id'].nunique()/logs.query('group==246')['user_id'].nunique()*100-100, 2), '%')
Относительная разница между группой 248 и 246: 2.13 %
Из результатов проверки видно, что в каждой из групп количество пользователей отличается, но не сильно. Максимальное отличие составляет 53 пользователя или примерно 2.13%. В целом деление трафика можно считать равномерным.
Проверим, есть ли в данных пользователи, которые числятся в 2 или 3 группах сразу:
logs.groupby(by='user_id', as_index=False).agg({'group': 'nunique'}).query('group>1')['user_id'].nunique()
0
Каждый из пользователей входит только в одну группу.
Предобработка данных показала, что данные довольно высокого качества, в них нет пропусков, 0,17% являются дубликатами, которые пришлось удалить.
Подготовка к проведению A/A/B-теста также была выполнена правильно, в целом выборка поделена на группы равномерно (по количеству пользователей группы отличаются друг от друг на 1-2%), а пользователей, которые состояли бы в нескольких группах, нет.
Исследование и проверка данных¶
print('Количество уникальных событий:', logs['event'].nunique())
print('Количество уникальных пользователей:', logs['user_id'].nunique())
Количество уникальных событий: 5 Количество уникальных пользователей: 7551
event_df = logs.groupby(by='user_id').agg({'event': ['nunique', 'count']}).droplevel(1, axis=1)
event_df.columns = ['nunique', 'count']
Проверим, есть ли в данных о событиях и действиях выбросы:
ax1 = event_df['nunique'].plot(kind='box', ax=plt.subplot(1,2,1), figsize=(15,5))
ax1.set(xticklabels=[])
ax1.set_ylabel('Количество событий')
ax1.set_title('Диаграмма размаха количества событий')
ax2 = event_df['count'].plot(kind='box', ax=plt.subplot(1,2,2), figsize=(15,5))
ax2.set(xticklabels=[])
ax2.set_ylabel('Количество действий')
ax2.set_title('Диаграмма размаха количества действий')
plt.show()
В данных о событиях выбросов нет, а в данных о действиях есть большое количество пользователей с аномально высокими показателями посещения мобильного приложения. Поэтому в случае с количеством событий можно ориентироваться на среднее количество, а для действий лучше использовать медианы для нивелирования влияния выбросов.
print('Среднее уникальных событий на пользователя:',
round(event_df['nunique'].mean(),2))
print('Среднее действий, совершаемых одним пользователем:',
round(event_df['count'].mean(),2))
Среднее уникальных событий на пользователя: 2.67 Среднее действий, совершаемых одним пользователем: 32.28
В логе всего бывает 5 разных видов событий, а уникальных пользователей всего 7551. На каждого пользователя в среднем приходится 2.67 события, а всего действий в среднем каждый пользователь совершает примерно 32.28.
print('Медиана уникальных событий на пользователя:',
round(event_df['nunique'].median(),2))
print('Медиана действий, совершаемых одним пользователем:',
round(event_df['count'].median(),2))
Медиана уникальных событий на пользователя: 3.0 Медиана действий, совершаемых одним пользователем: 20.0
Медиана количества посещений пользователем равна 3, а медиана действий равна 20. Это подтверждает влияние выбросов на среднее и медиану: для событий медиана от среднего отличается несильно (3 против 2.67), а для действий отличие большое (20 против 32.28).
print('Дата начала периода:', logs['dt'].min().strftime('%Y-%m-%d'))
print('Дата окончания периода:', logs['dt'].max().strftime('%Y-%m-%d'))
Дата начала периода: 2019-07-25 Дата окончания периода: 2019-08-08
event_count = logs.pivot_table(index='dt', columns='group', values='event', aggfunc='count')
fig, ax = plt.subplots(figsize=(10,5))
ax.bar(event_count.index, event_count[246], color='r', label=246)
ax.bar(event_count.index, event_count[247], bottom=event_count[246], color='b', label=247)
ax.bar(event_count.index, event_count[248], bottom=event_count[246]+event_count[247], color='y', label=248)
ax.xaxis_date()
ax.set_xlabel('Дата')
ax.set_ylabel('Количество событий')
ax.set_title('Динамика количества событий по группам')
fig.autofmt_xdate()
plt.legend()
plt.show()
На столбчатой диаграмме видно, что до 31 июля включительно данных в логе намного меньше, чем после 31 июля. Соответственно данные до 1 августа необходимо отбросить и рассматривать период с 1 по 7 августа включительно. На графике также четко видно, что в выборке остаются пользователи из всех 3 экспериментальных групп.
logs_new=logs[logs['dt']>='2019-08-01']
print('Количество удаленных из лога записей:', len(logs)-len(logs_new))
print('Доля удаленных из лога записей:', round((len(logs)-len(logs_new))/len(logs)*100,2), '%')
Количество удаленных из лога записей: 1989 Доля удаленных из лога записей: 0.82 %
print('Количество удаленных из лога пользователей:', logs['user_id'].nunique()-logs_new['user_id'].nunique())
print('Доля удаленных из лога пользователей:',
round((logs['user_id'].nunique()-logs_new['user_id'].nunique())/logs['user_id'].nunique()*100,2), '%')
Количество удаленных из лога пользователей: 13 Доля удаленных из лога пользователей: 0.17 %
Из лога было удалено 2826 записей, что составляет лишь 1,16% от общего количества всех событий. А пользователей было потеряно 17, что составляет 0,23% от общего количества пользователей в данных. Это еще раз подтверждает, что в отброшенном периоде данные неполные и их настолько мало, что лучше их убрать.
В логе представлены данные за период с 2019.07.25 до 2019.08.07. На каждого пользователя в среднем приходится примерно 2,67 события, а медиана действий составляет 20. Оказалось, что за период с 2019.07.25 по 2019.07.31 данных очень мало, они составляют лишь 1,16% от общего количества данных, поэтому данные скорее всего являются неполными, и их можно удалить. После удаления данных также удается сохранить примерно равномерное распределение пользователей по группам.
Изучение воронки событий¶
Построение цепочки событий¶
events_df = logs_new.groupby('event').agg({'user_id':['count', 'nunique']}).droplevel(1, axis=1)
events_df.columns = ['total', 'user_cnt']
events_df['conversion'] = round(events_df['user_cnt']/logs_new['user_id'].nunique()*100,2)
ax = events_df.sort_values(by='total')['total'].plot(kind='barh', figsize=(10,10), ax=plt.subplot(3,1,1))
ax.set_xlabel('Количество событий')
ax.set_ylabel('Событие')
ax.set_title('Частота событий')
ax1 = events_df.sort_values(by='total')['user_cnt'].plot(kind='barh', figsize=(10,10), ax=plt.subplot(3,1,2))
ax1.set_xlabel('Количество пользователей')
ax1.set_ylabel('Событие')
ax1.set_title('Количество пользователей, совершивших событие')
ax2 = events_df.sort_values(by='total')['conversion'].plot(kind='barh', figsize=(10,10), ax=plt.subplot(3,1,3))
ax2.set_xlabel('Доля пользователей, %')
ax2.set_ylabel('Событие')
ax2.set_title('Доля пользователей, совершивших событие')
plt.tight_layout()
plt.show()
events_df = events_df.sort_values(by='total', ascending=False)
events_df
| total | user_cnt | conversion | |
|---|---|---|---|
| event | |||
| MainScreenAppear | 117889 | 7423 | 98.47 |
| OffersScreenAppear | 46531 | 4597 | 60.98 |
| CartScreenAppear | 42343 | 3736 | 49.56 |
| PaymentScreenSuccessful | 33951 | 3540 | 46.96 |
| Tutorial | 1010 | 843 | 11.18 |
На построенных графиках видно, что самым популярным событием является переход на главную страницу, оно встречается чаще всего (117 328 раз) и 98,47% пользователей прошли через этот этап. Далее следуют страница с предложениями (46 333 раз и 60,96% пользователей), страница с корзиной (42 303 раза и 49,56% пользователей) и страница с успешной оплатой (33 918 раз и 46,97% пользователей). Получившаяся на данных последовательность подтверждает логику работы интернет-магазина, поэтому есть основание предположить, что события выстраиваются в цепочку именно таким образом.
Выбивается из общей последовательности страница с обучением, которая является наименее популярной и сильно уступает всем остальным страницам по всем показателям (1 005 раз и 11,15% пользователей). Однако вряд ли причиной столь редкого посещения страницы с обучением является ее посещение после усешной покупки. Очевидно, причина в том, что крайне мало пользователей проходит обучение перед использованием приложения. Соответственно при расчете воронки это событие учитывать не будем.
Расчет воронки¶
events_df['funnel'] = round((events_df['user_cnt'].pct_change() + 1)*100,2)
funnel_df = events_df[events_df.index!='Tutorial']
funnel_df.index=['Основная страница', 'Предложения', 'Корзина', 'Успешный платеж']
funnel_df
| total | user_cnt | conversion | funnel | |
|---|---|---|---|---|
| Основная страница | 117889 | 7423 | 98.47 | NaN |
| Предложения | 46531 | 4597 | 60.98 | 61.93 |
| Корзина | 42343 | 3736 | 49.56 | 81.27 |
| Успешный платеж | 33951 | 3540 | 46.96 | 94.75 |
fig = go.Figure(go.Funnel(
y = funnel_df.index,
x = funnel_df['user_cnt'],
textposition = "inside",
textinfo = "value+percent previous",
))
fig.update_layout(title={
'text': "Пользовательская воронка событий",
'y': 0.9,
'x': 0.53})
fig.show()
На графике видно, что больше всего пользователей теряется при переходе от основной странице к странице с предложениями товара (62% пользователей переходят на нее). То есть примерно треть пользователей посещает главную страницу сайта, а затем уходит.
Примерно 81% пользователей, просматривавших предложения товара, добавляют его в корзину. Логично, что эта доля выше, чем при переходе от основной страницы к предложениям, так как если пользователь просматривает предложения товара, то он уже действительно задумался о покупке и вероятно найдет то, что ему нужно.
95% пользователей, добавивших товар в корзину, успешно оплатили его. Если пользователь при выборе товара добавил его в корзину, то вероятность того, что товар он не купит, крайне мала.
В целом до успешной оплаты товара доходят примерно 47,7% пользователей, посетивших основную страницу, что является очень хорошим показателем конверсии посетителя в покупателя.
Также необходимо еще раз отметить, что, например, не все пользователи заходили на основную страницу (98,47% заходили). Это означает, что какие-то пользователи перешли сразу на другой этап, скорее всего на страницу с предложением товара. То есть при расчете воронки есть некоторая погрешность, однако ей можно пренебречь, так как большинство пользователей проходят именно через этот путь.
Из анализа данных о событиях следует, что в логе четко прослеживается определенная цепочка событий. При этом больше всего пользователей (38%) теряется при переходе с основной страницы на страницу с товаром, а до платежа доходят 47,7% (или 47% от общего количества) пользователей, что является очень высоким показателем конверсии посетителя в покупателя. Также стоит отметить, что не все пользователи проходят через рассмотренную воронку, однако процент таких пользователей очень мал (например на основной странице побывали 98,47% всех пользователей).
Изучение результатов A/A/B-теста¶
logs_new.groupby(by='group').agg({'user_id':'nunique'})
| user_id | |
|---|---|
| group | |
| 246 | 2484 |
| 247 | 2517 |
| 248 | 2537 |
Выбор уровня значимости с учетом множественности проверок¶
Для того чтобы выбрать правильный уровень значимости с учетом множественности проверок, необходимо определиться с количеством проверяемых на одних и тех же данных гипотез. Что касается гипотез о долях, то их необходимо проверить попарно для каждого события и каждой группы. Вариантов с группами всего 4 (A1 и A2; A1 и B; A2 и B; (A1+A2) и B), а событий всего 5. Поэтому гипотез с долями всего будет 5 * 4 = 20.
Для лучшей проверки контрольных групп A1 и A2 можно также сравнить их средние количества событий и действий, совершаемых пользователями, а также средние времени, в которое пользователи предпочитают заходить на сайт. Это еще 3 проверки. Таким образом, общее количество проверок на одних и тех же данных составляет 23.
Поправка Бонферрони силнее всего снижает мощность теста.
Поправка Холма и иные пошаговые методы являются наиболее подходящими с точки зрения мощности теста методами, однако в силу более сложного расчета уровня значимости и необходимости ранжирования гипотез по возрастанию p-value использование данного метода оправдано лишь в случае спорных значений p-value.
Поэтому для первого тестирования всех гипотез лушче всего использовать поправку Шидака, так как она меньше Бонферрони снижает мощность теста, а уровень значимости одинаков для всех проверок в отличие от метода Холма. Возьмем базовый уровень значимости равным 0.05. Тогда с учетом поправки и количества гипотез уровень значимости получается равным следующему числу:
alpha = 0.05
m = 23
alpha_new = 1-(1-alpha)**(1/m)
print('Уровень значимости:', round(alpha_new,4))
Уровень значимости: 0.0022
Проверка контрольных групп A/A¶
sampleA1 = logs_new[logs_new['group'] == 246].groupby(by='user_id').agg({'event':['nunique', 'count']})
sampleA2 = logs_new[logs_new['group'] == 247].groupby(by='user_id').agg({'event':['nunique', 'count']})
sampleA1 = sampleA1.droplevel(1,axis=1)
sampleA2 = sampleA2.droplevel(1,axis=1)
sampleA1.columns = ['unique', 'total']
sampleA2.columns = ['unique', 'total']
Среднее количество действий¶
Сформулируем гипотезы:
H0: Средние количества действий по пользователям в группах 246 и 247 равны.
H1: Средние количества действий по пользователям в группах 246 и 247 не равны.
Проверим нормальность распределения данных для количества действий с помощью гистограмм и теста Шапиро-Уилка.
Избавимся от аномалий, отбросим данные после 95-ого перцентиля.
percA1 = np.percentile(sampleA1['total'], [90,95,99])
percA2 = np.percentile(sampleA2['total'], [90,95,99])
ax = sampleA1[sampleA1['total']<=percA1[1]]['total'].plot(kind='hist', figsize=(10,5), ax=plt.subplot(1,2,1))
ax.set_xlabel('Количество действий')
ax.set_ylabel('Количество пользователей')
ax.set_title('Гистограмма распределения количества действий A1')
ax1 = sampleA2[sampleA2['total']<=percA2[1]]['total'].plot(kind='hist', figsize=(15,5), ax=plt.subplot(1,2,2))
ax1.set_xlabel('Количество действий')
ax1.set_ylabel('Количество пользователей')
ax1.set_title('Гистограмма распределения количества действий A2')
plt.show()
print('A1:', st.shapiro(sampleA1[sampleA1['total']<=percA1[1]]['total']))
print('A2:', st.shapiro(sampleA2[sampleA2['total']<=percA2[1]]['total']))
A1: ShapiroResult(statistic=0.8910079598426819, pvalue=5.833091510707499e-38) A2: ShapiroResult(statistic=0.8850290775299072, pvalue=5.980629741861173e-39)
Обе контрольные группы распределены ненормально. На гистограмме четко видно, что чем меньше количество действий, тем больше пользователей в каждой из выборок. Из теста Шапиро-Уилка также следует вывод, что гипотезу о нормальности отклоняем (p-value < 0.0022 для каждой выборки).
Таким образом, для проверки гипотезы о равенстве средних следует применять непараметрический тест Манна-Уитни.
print('p-value:', '{0:.5f}'.format(st.mannwhitneyu(sampleA1['total'], sampleA2['total'])[1]))
print('Относительная разница среднего количества действий A1 к A2:',
'{0:.3f}'.format(sampleA2['total'].mean()/sampleA1['total'].mean()-1))
p-value: 0.80661 Относительная разница среднего количества действий A1 к A2: -0.041
print('p-value:', '{0:.5f}'.format(st.mannwhitneyu(sampleA1[sampleA1['total']<=percA1[1]]['total'],
sampleA2[sampleA2['total']<=percA2[1]]['total'])[1]))
print('Относительная разница среднего количества действий A1 к A2:',
'{0:.3f}'.format(sampleA2[sampleA2['total']<=percA2[1]]['total'].mean()/
sampleA1[sampleA1['total']<=percA1[1]]['total'].mean()-1))
p-value: 0.83474 Относительная разница среднего количества действий A1 к A2: -0.007
Как с учетом выбросов, так и без них получаем, что разница между выборками (4% с аномалиями и 0,4% без учета аномалий) не является статистически значимой (p-value > 0.0022). Поэтому по количеству действий существенной разницы между контрольными группами нет. Это также подтверждается распределением данных на гистограммах.
Среднее количество событий¶
Сформулируем гипотезы:
H0: Средние количества событий по пользователям в группах 246 и 247 равны.
H1: Средние количества событий по пользователям в группах 246 и 247 не равны.
Проверим нормальность распределения данных для количества действий с помощью гистограмм и теста Шапиро-Уилка.
Посмотрим, есть ли аномалии:
print('A1:', np.percentile(sampleA1['unique'], [90,95,99]))
print('A2:', np.percentile(sampleA2['unique'], [90,95,99]))
A1: [4. 5. 5.] A2: [4. 5. 5.]
95-й перцентиль равен 5 событиям (максимально возможное число), поэтому аномалий в данных нет.
ax = sampleA1['unique'].plot(kind='hist', figsize=(10,5), ax=plt.subplot(1,2,1), bins=5)
ax.set_xlabel('Количество событий')
ax.set_ylabel('Количество пользователей')
ax.set_title('Гистограмма распределения количества событий A1')
ax1 = sampleA2['unique'].plot(kind='hist', figsize=(15,5), ax=plt.subplot(1,2,2), bins=5)
ax1.set_xlabel('Количество событий')
ax1.set_ylabel('Количество пользователей')
ax1.set_title('Гистограмма распределения количества событий A2')
plt.show()
print('A1:', st.shapiro(sampleA1[sampleA1['unique']<=percA1[1]]['unique']))
print('A2:', st.shapiro(sampleA2[sampleA2['unique']<=percA2[1]]['unique']))
A1: ShapiroResult(statistic=0.7864741086959839, pvalue=0.0) A2: ShapiroResult(statistic=0.7911558151245117, pvalue=0.0)
Обе контрольные группы распределены ненормально. На гистограмме четко видно, что распределение не напоминает "колокол". Из теста Шапиро-Уилка также следует вывод, что гипотезу о нормальности отклоняем (p-value < 0.0022 для каждой выборки).
Таким образом, для проверки гипотезы о равенстве средних следует применять непараметрический тест Манна-Уитни.
print('p-value:', '{0:.5f}'.format(st.mannwhitneyu(sampleA1['unique'], sampleA2['unique'])[1]))
print('Относительная разница среднего количества действий A1 к A2:',
'{0:.3f}'.format(sampleA2['unique'].mean()/sampleA1['unique'].mean()-1))
p-value: 0.17196 Относительная разница среднего количества действий A1 к A2: -0.021
Получаем, что разница между выборками в 2% не является статистически значимой (p-value > 0.0022). Поэтому по количеству событий существенной разницы между контрольными группами нет. Это также подтверждается распределением данных на гистограммах.
Среднее время посещения сайта¶
Сформулируем гипотезы:
H0: Среднее время посещения сайта в группах 246 и 247 одинаковое.
H1: Среднее время посещения сайта в группах 246 и 247 разное.
Проверим нормальность распределения данных для количества действий с помощью гистограмм и теста Шапиро-Уилка. Для расчетов возьмем время в секундах в формате timestamp.
ax = logs_new[logs_new['group']==246]['timestamp'].plot(kind='hist', figsize=(10,5), ax=plt.subplot(1,2,1))
ax.set_xlabel('Время посещения в секундах с 01.01.1970')
ax.set_ylabel('Количество пользователей')
ax.set_title('Гистограмма распределения времени посещения A1')
ax1 = logs_new[logs_new['group']==247]['timestamp'].plot(kind='hist', figsize=(15,5), ax=plt.subplot(1,2,2))
ax1.set_xlabel('Время посещения в секундах с 01.01.1970')
ax1.set_ylabel('Количество пользователей')
ax1.set_title('Гистограмма распределения времени посещения A2')
plt.show()
print('A1:', st.shapiro(logs_new[logs_new['group']==246]['timestamp']))
print('A2:', st.shapiro(logs_new[logs_new['group']==247]['timestamp']))
A1: ShapiroResult(statistic=0.03165018558502197, pvalue=0.0) A2: ShapiroResult(statistic=0.021876096725463867, pvalue=0.0)
C:\anaconda\envs\practicum\lib\site-packages\scipy\stats\_morestats.py:1761: UserWarning: p-value may not be accurate for N > 5000. C:\anaconda\envs\practicum\lib\site-packages\scipy\stats\_morestats.py:1761: UserWarning: p-value may not be accurate for N > 5000.
Обе контрольные группы распределены ненормально. На гистограмме четко видно, что распределение не напоминает "колокол". Из теста Шапиро-Уилка также следует вывод, что гипотезу о нормальности отклоняем (p-value < 0.0022 для каждой выборки).
Таким образом, для проверки гипотезы о равенстве средних следует применять непараметрический тест Манна-Уитни.
print('p-value:', '{0:.15f}'.format(st.mannwhitneyu(logs_new[logs_new['group']==246]['timestamp'],
logs_new[logs_new['group']==247]['timestamp'])[1]))
print('Относительная разница времени посещения A2 к A1:',
'{0:.7f}'.format(logs_new[logs_new['group']==247]['timestamp'].mean()/
logs_new[logs_new['group']==246]['timestamp'].mean()-1))
p-value: 0.000000000000013 Относительная разница времени посещения A2 к A1: -0.0000044
По времени посещения разница очень маленькая (0.00043%). Объясняется это скорее всего тем, что время исчисляется в миллиардах секунд, и соответственно разница в несколько секунд в относительном выражении будет крайне мала. Однако при этом она является статистически значимой (p-value < 0.0022).
Если обратиться к гистограммам распределения данных, то на них заметно, что пользователи распределны похожим образом. Среди отличий можно лишь отметить, что лидерство времени, которое примерно приходится на 5 августа (1.5650e^9) в группе A1 выражено более ярко, чем в группе А2.
Результат теста скорее всего обусловлен большим количеством наблюдений и их повышенной чувствительностью к небольшим различиям. На гистограмме очень хорошо видно, что на самом деле по времени посещения сайта выборки мало чем отличаются друг от друга.
Доли совершивших события¶
Сформулируем гипотезы:
H0: Доли совершивших события в контрольных группах 246 и 247 равны.
H1: Доли совершивших события в контрольных группах 246 и 247 разные.
Проверять гипотезы будем с помощью z-теста.
def z_testing(df, alpha):
for row in range(len(df)):
p_value = pr.proportions_ztest([df.loc[row, 'success1'], df.loc[row, 'success2']], [df.loc[row, 'trial1'], df.loc[row, 'trial2']])[1]
print(df.loc[row, 'event'], 'p-значение:', round(p_value,5))
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными')
return
def df_creater(group1, group2, group3):
df = logs_new[logs_new['group']!=group1].pivot_table(index='event', values='user_id', columns='group', aggfunc='nunique')
df.columns = ['success1', 'success2']
df['trial1'] = logs_new[logs_new['group']==group2]['user_id'].nunique()
df['trial2'] = logs_new[logs_new['group']==group3]['user_id'].nunique()
df['conversion1'] = round(df['success1']/df['trial1']*100,2)
df['conversion2'] = round(df['success2']/df['trial2']*100,2)
df = df.sort_values(by='success1', ascending = False).reset_index()
return df
def bar_creater(df):
n = len(df)
r = np.arange(n)
width = 0.25
fig,ax = plt.subplots(figsize=(15,8))
ax=plt.bar(r, df['conversion1'], width=width, label='conversion1')
ax=plt.bar(r + width, df['conversion2'], width=width, label='conversion2')
plt.xlabel('Событие')
plt.ylabel('Конверсия, %')
plt.title('Конверсия в события по группам')
plt.xticks(r + width/2, df['event'])
plt.legend()
plt.show()
return
aa_test = df_creater(248, 246, 247)
aa_test
| event | success1 | success2 | trial1 | trial2 | conversion1 | conversion2 | |
|---|---|---|---|---|---|---|---|
| 0 | MainScreenAppear | 2450 | 2479 | 2484 | 2517 | 98.63 | 98.49 |
| 1 | OffersScreenAppear | 1542 | 1524 | 2484 | 2517 | 62.08 | 60.55 |
| 2 | CartScreenAppear | 1266 | 1239 | 2484 | 2517 | 50.97 | 49.23 |
| 3 | PaymentScreenSuccessful | 1200 | 1158 | 2484 | 2517 | 48.31 | 46.01 |
| 4 | Tutorial | 278 | 284 | 2484 | 2517 | 11.19 | 11.28 |
aa_conversion = bar_creater(aa_test)
z_testing(aa_test, alpha_new)
MainScreenAppear p-значение: 0.67562 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными OffersScreenAppear p-значение: 0.26699 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными CartScreenAppear p-значение: 0.21828 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными PaymentScreenSuccessful p-значение: 0.10298 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Tutorial p-значение: 0.91828 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
z-тест показал, что для всех событий разница в долях между контрольными группами не является статистически значимой (p-value < 0.0022), то есть по долям группы 246 и 247 также примерно одинаковые.
Самым популярным событием является посещение главной страница приложения. В каждой из контрольных групп переходили на главную страницу хотя бы раз 2450 из 2484 пользователей (98,63%) и 2476 из 2513 пользователей (98,53%) соответственно.
Все проведенные тесты показали, что контрольные группы 246 и 247 почти неотличимы друг от друга.
Отдельно стоит отметить, что на данный вывод не влияет выбор уровня значимости. Даже при alpha=0.05 все равно p-value > alpha по всем тестам, кроме времени посещения сайта, которое тем не менее все равно распределено примерно одинаково (это заметно на гистограммах распределения).
Таким образом, разбиение на контрольные группы работает корректно.
Сравнение долей соверших события в экспериментальной группе с контрольными¶
Для удобства в использовании функций возьмем контрольную группу за 1, а экспериментальную за 2.
Сравнение долей совершивших события в группах 248 и 246¶
a1b_test = df_creater(247, 246, 248)
a1b_test
| event | success1 | success2 | trial1 | trial2 | conversion1 | conversion2 | |
|---|---|---|---|---|---|---|---|
| 0 | MainScreenAppear | 2450 | 2494 | 2484 | 2537 | 98.63 | 98.31 |
| 1 | OffersScreenAppear | 1542 | 1531 | 2484 | 2537 | 62.08 | 60.35 |
| 2 | CartScreenAppear | 1266 | 1231 | 2484 | 2537 | 50.97 | 48.52 |
| 3 | PaymentScreenSuccessful | 1200 | 1182 | 2484 | 2537 | 48.31 | 46.59 |
| 4 | Tutorial | 278 | 281 | 2484 | 2537 | 11.19 | 11.08 |
a1b_conversion = bar_creater(a1b_test)
z_testing(a1b_test, alpha_new)
MainScreenAppear p-значение: 0.34706 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными OffersScreenAppear p-значение: 0.20836 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными CartScreenAppear p-значение: 0.08328 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными PaymentScreenSuccessful p-значение: 0.22269 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Tutorial p-значение: 0.89645 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
z-тест показал, что для всех событий разница в долях между группами 248 и 246 не является статистически значимой (p-value < 0.0022). Более того, речь идет о падении по сравнению с контрольной группой по всем событиям.
Отдельно стоит отметить, что на данный вывод не влияет выбор уровня значимости. Даже при alpha=0.05 все равно p-value > alpha по всем событиям.
Таким образом, по сравнению с группой 246 введение нового шрифта не привело к каким-либо положительным изменениям с точки зрения долей совершающих события пользователей.
Сравнение долей совершивших события в группах 248 и 247¶
a2b_test = df_creater(246, 247, 248)
a2b_test
| event | success1 | success2 | trial1 | trial2 | conversion1 | conversion2 | |
|---|---|---|---|---|---|---|---|
| 0 | MainScreenAppear | 2479 | 2494 | 2517 | 2537 | 98.49 | 98.31 |
| 1 | OffersScreenAppear | 1524 | 1531 | 2517 | 2537 | 60.55 | 60.35 |
| 2 | CartScreenAppear | 1239 | 1231 | 2517 | 2537 | 49.23 | 48.52 |
| 3 | PaymentScreenSuccessful | 1158 | 1182 | 2517 | 2537 | 46.01 | 46.59 |
| 4 | Tutorial | 284 | 281 | 2517 | 2537 | 11.28 | 11.08 |
a2b_conversion = bar_creater(a2b_test)
z_testing(a2b_test, alpha_new)
MainScreenAppear p-значение: 0.60017 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными OffersScreenAppear p-значение: 0.8836 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными CartScreenAppear p-значение: 0.61695 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными PaymentScreenSuccessful p-значение: 0.67754 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Tutorial p-значение: 0.8152 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
z-тест показал, что для всех событий разница в долях между группами 248 и 247 не является статистически значимой (p-value < 0.0022). Более того, речь идет о падении по сравнению с контрольной группой по всем событиям, кроме страница платежа, где наблюдается небольшой рост с 46,08% до 46,55%, но он также не является статистически значимым.
Отдельно стоит отметить, что на данный вывод не влияет выбор уровня значимости. Даже при alpha=0.05 все равно p-value > alpha по всем событиям.
Таким образом, по сравнению с группой 247 введение нового шрифта не привело к каким-либо положительным изменениям с точки зрения долей совершающих события пользователей.
Сравнение долей совершивших события в группах 248 и (246+247)¶
ab_test = logs_new.pivot_table(index='event', values='user_id', columns='group', aggfunc='nunique')
ab_test['246+247'] = ab_test[246] + ab_test[247]
ab_test = ab_test.drop(columns=[246, 247])
ab_test.columns = ['success2', 'success1']
ab_test['trial2'] = logs_new[logs_new['group']==248]['user_id'].nunique()
ab_test['trial1'] = logs_new[logs_new['group']!=248]['user_id'].nunique()
ab_test['conversion2'] = round(ab_test['success2']/ab_test['trial2']*100,2)
ab_test['conversion1'] = round(ab_test['success1']/ab_test['trial1']*100,2)
ab_test = ab_test.sort_values(by='success2', ascending = False).reset_index()
ab_test
| event | success2 | success1 | trial2 | trial1 | conversion2 | conversion1 | |
|---|---|---|---|---|---|---|---|
| 0 | MainScreenAppear | 2494 | 4929 | 2537 | 5001 | 98.31 | 98.56 |
| 1 | OffersScreenAppear | 1531 | 3066 | 2537 | 5001 | 60.35 | 61.31 |
| 2 | CartScreenAppear | 1231 | 2505 | 2537 | 5001 | 48.52 | 50.09 |
| 3 | PaymentScreenSuccessful | 1182 | 2358 | 2537 | 5001 | 46.59 | 47.15 |
| 4 | Tutorial | 281 | 562 | 2537 | 5001 | 11.08 | 11.24 |
ab_conversion = bar_creater(ab_test)
z_testing(ab_test, alpha_new)
MainScreenAppear p-значение: 0.39299 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными OffersScreenAppear p-значение: 0.419 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными CartScreenAppear p-значение: 0.19819 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными PaymentScreenSuccessful p-значение: 0.64521 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Tutorial p-значение: 0.8333 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
z-тест показал, что для всех событий разница в долях между экспериментальной группой 248 и контрольными группами 246 и 247 вместе не является статистически значимой (p-value < 0.0022). При этом важно отметить, что в экспериментальной группе наблюдается снижение долей по всем событиям.
Отдельно стоит отметить, что на данный вывод не влияет выбор уровня значимости. Даже при alpha=0.05 все равно p-value > alpha по всем событиям.
Таким образом, по сравнению с контрольной группой введение нового шрифта в экспериментальной группе не привело к каким-либо положительным изменениям с точки зрения долей совершающих события пользователей.
Решение по результатам A/A/B-теста¶
Результат A/A/B-теста является однозначным: вводить новый шрифт не нужно, так как это не привело к статистически значимому увеличению конверсии пользователей ни по одному из осуществляемых на сайте событий.
Единственное выявленное при сравнении групп 248 и 247 увеличение доли покупателей с 46,08% до 46,55% статистически значимым не является. Во всех остальных случаях наблюдается снижение доли покупателей.
Результаты теста также подтверждают, что использование поправки Холма не имеет смысла, так как p-value по всем гипотезам либо больше 0.05, либо сильно меньше значений, используемых в других непошаговых поправках (например, 0.0022).
Выводы¶
- Предобработка данных показала, что представленные в логе данные довольно высокого качества: в них отсутствуют пропуски, а количество явных дубликатов составляет 0,17%, поэтому они были удалены;
- Подготовка к проведениию A/A/B-теста была проведена корректно: выборка поделена на группы равномерно (разница между группами составляет 1-2%), а пользователей, состоящих в 2 или 3 группах сразу, нет;
- В рассматриваемом двухнедельном периоде на каждого пользователя в среднем приходится примерно 2,67 события, а медиана действий равна 20;
- На первую неделю периода приходится всего лишь 1,16% от общего количества данных, что говорит о высокой вероятности их неполноты за данный период, поэтому в целях анализа рассматривалась только вторая половина периода без нарушения равномерного распределения пользователей по группам;
- Пользователь в рамках пользования приложением проходит следующий путь (в скобках указана доля перешедших на данный этап из предыдущего): основная страница -> предложения (62%) -> корзина (81%) -> оплата товара (95%). При этом на основной странице побывали 98,47% пользователей, т.е. не все пользователи проходят данный путь, а до оплаты товара доходили 47% от общего количества пользователей, что является крайне высоким показателем конверсии посетителей в покупателей;
- A/A/B тест показал, что вводить новый шрифт нецелесообразно: почти по всем событиям в результате нововведений конверсия упала (в 14 из 15 случаях), при этом ни один из результатов не является статистически значимым. Контрольные группы были сформированы корректно и A/A тест показал правильность всех механизмов и расчетов в рамках эксперимента.